﻿using System;
using System.Linq;
using ch07.binary.type;

namespace ch07.binary.interpreter
{
    public struct Vm
    {
        internal OperandStack OperandStack;
        private ControlStack _controlStack;
        internal Module Module;
        internal Memory Memory;
        public GlobalVar[] Globals;
        public uint Local0Idx;

        public Vm(Module mo) : this()
        {
            OperandStack = new OperandStack {Slots = Array.Empty<ulong>()};
            _controlStack = new ControlStack(Array.Empty<ControlFrame>());
            Module = mo;
            Globals ??= Array.Empty<GlobalVar>();
        }

        internal void Loop()
        {
            var depth = _controlStack.controlDepth();
            // var cf = ControlStack.topControlFrame();
            for (; _controlStack.controlDepth() >= depth;)
            {
                if (_controlStack.Frames[^1].pc == _controlStack.Frames[^1].instrs.Length)
                {
                    ExitBlock();
                }
                else
                {
                    var instr = _controlStack.Frames[^1].instrs[_controlStack.Frames[^1].pc];
                    // ControlStack.frames[^1].pc++;
                    _controlStack.Frames[^1].pc++;
                    ExecInstr(instr);
                }
            }
        }

        public void InitMem()
        {
            if (Module.MemSec?.Length > 0)
            {
                Memory = new Memory(Module.MemSec[0]);
            }

            if (Module.DataSec == null)
            {
                return;
            }

            foreach (var data in Module.DataSec)
            {
                foreach (var instr in data.Offset)
                {
                    ExecInstr(instr);
                }

                Memory.Write(OperandStack.PopU64(), data.Init);
            }
        }

        public void InitGlobals()
        {
            if (Module.GlobalSec == null)
            {
                return;
            }

            foreach (var global in Module.GlobalSec)
            {
                foreach (var instr in global.Init)
                {
                    ExecInstr(instr);
                }

                Globals = Globals.Append(new GlobalVar {_type = global.Type, val = PopU64()}).ToArray();
            }
        }


        private void ExecInstr(Instruction instr)
        {
            //fmt.Printf("%s %v\n", instr.GetOpname(), instr.Args)
            var op = Instr.GetInstrFn()[instr.Opcode];
            op(ref this, instr.Args);
        }

        /* block stack */
        public void EnterBlock(byte opcode, ref FuncType bt, ref Instruction[] instrs)
        {
            var bp = OperandStack.StackSize() - bt.ParamTypes.Length;
            var cf = new ControlFrame(opcode, ref bt, ref instrs, bp);
            _controlStack.pushControlFrame(ref cf);
            if (opcode == OpCodes.Call)
            {
                Local0Idx = (uint) bp;
            }
        }

        public void ExitBlock()
        {
            var cf = _controlStack.popControlFrame();
            ClearBlock(ref cf);
        }

        public void ClearBlock(ref ControlFrame cf)
        {
            var results = PopU64S(cf.bt.ResultTypes.Length);
            PopU64S(OperandStack.StackSize() - cf.bp);
            PushU64S(results);
            if (cf.opcode != OpCodes.Call || _controlStack.controlDepth() <= 0) return;
            var (lastCallFrame, _) = _controlStack.TopCallFrame();
            if (lastCallFrame?.bp != null) Local0Idx = (uint) lastCallFrame?.bp;
        }

        private ulong[] PopU64S(int n)
        {
            return OperandStack.popU64s(n);
        }

        private void PushU64S(ulong[] vals)
        {
            OperandStack.pushU64s(vals);
        }

        public ulong GetOperand(uint idx)
        {
            return OperandStack.getOperand(idx);
        }

        public ulong PopU64()
        {
            return OperandStack.PopU64();
        }

        public void PushU64(ulong val)
        {
            OperandStack.PushU64(val);
        }

        public void PushU32(uint val)
        {
            OperandStack.PushU32(val);
        }

        public void PushS32(int val)
        {
            OperandStack.PushS32(val);
        }

        public void PushS64(long val)
        {
            OperandStack.PushS64(val);
        }

        public bool PopBool()
        {
            return OperandStack.PopBool();
        }
    }
}